package org.mijia.dotsbox.core.search;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.FSDirectory;
import org.mijia.dotsbox.utils.LogUtils;

public final class SearchBlock {

	private final File indexDir;
	private final Properties searchProp;
	private final int optimizeTimeout;
	private final int searcherUpdateTimeout;
	
	private IndexWriter indexWriter;
	
	private boolean toOptimize = false;
	private boolean toUpdateSearcher = false;
	
	private ExecutorService indexWorker;
	private ScheduledExecutorService timerWorker;
	
	public SearchBlock(File dataDir, Properties prop) {
		assert(dataDir != null);
		
		indexDir = dataDir;
		searchProp = prop;
		
		optimizeTimeout = Integer.parseInt(searchProp.getProperty("block.index.optimize.timeout", "5")) * 60;
		searcherUpdateTimeout = Integer.parseInt(searchProp.getProperty("block.search.update.timeout", "2")) * 60;
		
		boolean maybeExist = indexDir.canRead() && indexDir.isDirectory();
		if (maybeExist) {
			String value = searchProp.getProperty("block.index.unlock", "false");
			boolean unlock = Boolean.parseBoolean(value);
			if (unlock) {
				// unlock the index at startup
				try {
					FSDirectory directory = FSDirectory.getDirectory(indexDir);
					if (IndexReader.isLocked(directory)) {
						LogUtils.info("Waring: The index directory " + indexDir.getAbsolutePath() + " is locked, unlocking ...");
						IndexReader.unlock(directory);
					}
				} catch (IOException e) {
					LogUtils.error("Error to unlock the index directory .. " + e.getMessage());
				}
			}
		}
		
		try {
			indexWriter = getIndexWriter();
		} catch (IOException e) {
			e.printStackTrace();
			LogUtils.error("Unable to get the index writer! " + e.getMessage());
			indexWriter = null;
		}
		
		indexWorker = Executors.newSingleThreadExecutor();
		timerWorker = Executors.newScheduledThreadPool(1);
		
		try {
			getIndexSearcher(false, false);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	// document CRUD
	public void addDocument(Document doc) {
		if (doc == null)
			return;
		
		indexWorker.execute(new AddDocumentTask(doc));
		scheduleOptimize();
		scheduleUpdateSearcher();
	}
	
	public void deleteDocument(int nDoc) {
		indexWorker.execute(new DeleteDocumentTask(nDoc));
		scheduleOptimize();
		scheduleUpdateSearcher();
	}
	
	public Document getDocument(int docId) {
		IndexReader reader = null;
		try {
			reader = IndexReader.open(indexDir);
			if (docId >= reader.maxDoc())
				return null;
			if (reader.isDeleted(docId)) 
				return null;
			return reader.document(docId);
		} catch (IOException e) {
			reader = null;
			e.printStackTrace();
			return null;
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
				}
			}
		}
	}
	
	private synchronized void scheduleOptimize() {
		if (toOptimize)
			return;
		
		Runnable submitTask = new Runnable() {
			public void run() {
				indexWorker.execute(new OptimizeTask());
			}
		};
		timerWorker.schedule(submitTask, optimizeTimeout, TimeUnit.SECONDS);
		toOptimize = true;
	}
	
	private synchronized void scheduleUpdateSearcher() {
		if (toUpdateSearcher)
			return;
		
		Runnable submitTask = new Runnable() {
			public void run() {
				indexWorker.execute(new UpdateSearcherTask());
			}
		};
		timerWorker.schedule(submitTask, searcherUpdateTimeout, TimeUnit.SECONDS);
		toUpdateSearcher = true;
	}
	
	// get index writer
	private IndexWriter getIndexWriter() throws IOException {
		boolean bCreate = !IndexReader.indexExists(indexDir);
		Analyzer analyzer = AnalyzerFactory.getDefaultAnalyzer();
		
		IndexWriter writer = new IndexWriter(indexDir, analyzer, bCreate);
		String value = searchProp.getProperty("block.index.mergeFactor", "10");
		writer.setMergeFactor(Integer.parseInt(value));
		
		value = searchProp.getProperty("block.index.useCompoundFile", "false");
		writer.setUseCompoundFile(Boolean.parseBoolean(value));
		
		value = searchProp.getProperty("block.index.maxBufferedDocs", "500");
		writer.setMaxBufferedDocs(Integer.parseInt(value));
		
		return writer;
	}
	
	public void dispose() {
		LogUtils.debug("Index and timer worker thread shuting down");
		indexWorker.shutdown();
		timerWorker.shutdown();
		
		// shutdown writer
		try {
			LogUtils.debug("Search block is closing. IndexWriter optimizing and closing");
			indexWriter.optimize();
			indexWriter.close();
		} catch (IOException e1) {
			LogUtils.warning("Error when closing the index writer! " + e1.getMessage());
		}
		
		// shutdown searcher
		try {
			closeSearcher();
		} catch (Exception e) {
			LogUtils.warning("Error when closing the index searcher! " + e.getMessage());
		}
	}
	
	// internal index worker tasks
	class AddDocumentTask implements Runnable {
		private final Document doc;
		
		public AddDocumentTask(Document doc) {
			this.doc = doc;
		}
		
		public void run() {
			try {
				indexWriter.addDocument(doc);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}
	
	class DeleteDocumentTask implements Runnable {
		private final int nDoc;
		
		public DeleteDocumentTask(int nDoc) {
			this.nDoc = nDoc;
		}
		
		public void run() {
			try {
				IndexReader reader = IndexReader.open(indexDir);
				reader.deleteDocument(nDoc);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	class OptimizeTask implements Runnable {

		public void run() {
			LogUtils.info("Optimizing the indices!");
			try {
				// FIXME: is this ok? including the update task because toOptimize is guarded by this
				synchronized (SearchBlock.this) {
					indexWriter.optimize();
					toOptimize = false;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}
	
	class UpdateSearcherTask implements Runnable {

		public void run() {
			LogUtils.info("Updating the searcher!");
			try {
				synchronized (SearchBlock.this) {
					getIndexSearcher(true, false);
					toUpdateSearcher = false;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
	}
	
	/**
     * Searcher Concurrent Control, taken from Solr and modified
     */
	private ReferCounter<IndexSearcher> searcher = null;
	private final ExecutorService searcherExecutor = Executors.newSingleThreadExecutor();
	private int pendingRequest = 0;
	private Object searcherLock = new Object();
	
    public ReferCounter<IndexSearcher> getIndexSearcher() {
        try {
            return getIndexSearcher(false, true);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * <p>
     * The registered searcher is the default searcher used to service queries.
     * A searcher will normally be registered after all of the warming
     * and event handlers (newSearcher or firstSearcher events) have run.
     * In the case where there is no registered searcher, the newly created searcher will
     * be registered before running the event handlers (a slow searcher is better than no searcher).
     *
     * <p>
     * If <tt>forceNew==true</tt> then
     *  A new searcher will be opened and registered irregardless if there is already
     *    a registered searcher or other searchers in the process of being created.
     * <p>
     * If <tt>forceNew==false</tt> then:<ul>
     *   <li>If a searcher is already registered, that searcher will be returned</li>
     *   <li>If no searcher is currently registered, but at least one is in the process of being created, then
     * this call will block until the first searcher is registered</li>
     *   <li>If no searcher is currently registered, and no searchers in the process of being registered, a new
     * searcher will be created.</li>
     * </ul>
     * <p>
     * If <tt>returnSearcher==true</tt> then a {@link ReferCounter}&lt;{@link SeseIndexSearcher}&gt; will be returned with
     * the reference count incremented.  It <b>must</b> be decremented when no longer needed.
     * <p>
	 *
     * @param forceNew           if true, force the open of a new index searcher regardless if there is already one open.
     * @param returnSearcher     if true, returns a {@link SeseIndexSearcher} holder with the refcount already incremented.
     * @throws IOException
     */
    public ReferCounter<IndexSearcher> getIndexSearcher(boolean forceNew, 
            boolean returnValue) throws Exception {
    	synchronized (searcherLock) {
    		if (searcher != null && !forceNew) {
    			if (returnValue) {
    				searcher.incre();
    				return searcher;
    			}
    			return null;
    		}
    		
    		// check if we have some searcher pending
    		if (pendingRequest > 0 && !forceNew && searcher == null) {
                try {
                    searcherLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
            // double check again
            if (searcher != null && !forceNew) {
                if (returnValue) {
                    searcher.incre();
                    return searcher;
                } 
                return null;
            }
            
            // we have to open a new searcher
            ++pendingRequest;
    	}
    	
    	IndexSearcher tmp;
    	try {
    		if (pendingRequest < 1) {
    			// sanity check error, should be not less than 1
    			pendingRequest = 1;
    		} else if (pendingRequest > 1) {
                System.out.println("PERFORMANCE WARNING: Searcher pending ... " + pendingRequest);
            }
    		tmp = new IndexSearcher(FSDirectory.getDirectory(indexDir));
    	} catch (Throwable e) {
    		synchronized (searcherLock) {
                --pendingRequest;
                searcherLock.notify();
            }
            throw new RuntimeException(e);
    	}
    	
    	final IndexSearcher newSearcher = tmp;
        ReferCounter<IndexSearcher> currHolder = null;
        final ReferCounter<IndexSearcher> newHolder = newReferCounter(newSearcher);
        if (returnValue) {
            newHolder.incre();
        }
        
        final boolean[] decrePendingSearchers = new boolean[1];
        decrePendingSearchers[0] = true;
        
        try {
            synchronized (searcherLock) {
                if (searcher == null) {
                    registerSearcher(newHolder);
                    decrePendingSearchers[0] = false;
                } else {
                    currHolder = searcher;
                    currHolder.incre();
                }
            }
            
            final ReferCounter<IndexSearcher> currHolderF = currHolder;
            if (currHolder != null) {
                searcherExecutor.submit(new Callable<Object>() {
                    public Object call() throws Exception {
                        try {
                            decrePendingSearchers[0] = false;
                            registerSearcher(newHolder);
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally {
                            currHolderF.decre();
                        }
                        return null;
                    }
                });
            }
            
            return returnValue ? newHolder : null;
        } catch (Exception e) {
            e.printStackTrace();
            if (currHolder != null) {
               currHolder.decre(); 
            }
            synchronized (searcherLock) {
                if (decrePendingSearchers[0]) {
                    --pendingRequest;
                } 
                if (pendingRequest < 0) {
                    // sanity check failed
                	pendingRequest = 0;
                }
                searcherLock.notify();
            }
            
            return returnValue ? newHolder : null;
        } 
    }
    
    private ReferCounter<IndexSearcher> newReferCounter(IndexSearcher newSearcher) {
    	ReferCounter<IndexSearcher> holder = new ReferCounter<IndexSearcher>(newSearcher) {
			@Override
			protected void close() throws Exception {
				resource.close();
			}
    	};
    	holder.incre();
    	return holder;
    }
    
    private void registerSearcher(ReferCounter<IndexSearcher> holder) throws IOException {
    	synchronized (searcherLock) {
    		try {
    			if (searcher != null) {
    				searcher.decre();
    				searcher = null;
    			}
    			searcher = holder;
    			LogUtils.info("New searcher registered in " + new Date().toString());
    		} catch (Exception e) {
    			e.printStackTrace();
    			LogUtils.error("Error to register index searcher " + e.getMessage());
    		} finally {
    			--pendingRequest;
    			searcherLock.notifyAll();
    		}
    	}
    }
    
    private void closeSearcher() throws Exception {
    	synchronized (searcherLock) {
    		if (searcher != null) {
    			searcher.decre();
    			searcher = null;
    		}
    	}
    }
}
